Add confirmed repository deletion tool - #3076
Conversation
Add a destructive delete_repository tool that requires an exact owner/repo confirmation through multi-round-trip elicitation. Gate the tool to MCP protocol 2026-07-28 and newer across local and remote transports. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8
There was a problem hiding this comment.
Pull request overview
Adds protocol-gated, elicitation-confirmed repository deletion.
Changes:
- Adds
delete_repositorywithdelete_reposcope and destructive annotations. - Filters tools by minimum MCP protocol version.
- Adds schema, unit, transport, and documentation updates.
Show a summary per file
| File | Description |
|---|---|
README.md |
Documents the new tool. |
pkg/scopes/scopes.go |
Defines delete_repo. |
pkg/scopes/scopes_test.go |
Tests scope expansion. |
pkg/inventory/server_tool.go |
Adds minimum protocol metadata. |
pkg/inventory/registry.go |
Installs protocol filtering. |
pkg/inventory/protocol_version.go |
Implements listing/call filtering. |
pkg/inventory/protocol_version_test.go |
Tests protocol gating. |
pkg/http/handler_test.go |
Tests HTTP tool visibility. |
pkg/github/tools.go |
Registers the deletion tool. |
pkg/github/repositories.go |
Implements confirmation and deletion. |
pkg/github/repositories_test.go |
Tests deletion and elicitation. |
pkg/github/helper_test.go |
Adds the mock endpoint constant. |
pkg/github/__toolsnaps__/delete_repository.snap |
Captures the tool schema. |
internal/ghmcp/oauth.go |
Reuses the protocol constant. |
Review details
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- Files reviewed: 14/14 changed files
- Comments generated: 1
- Review effort level: Balanced
Gate protocol-restricted tools on required elicitation capabilities and enforce direct calls inside the registered handler so SDK result finalization remains intact. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8
Seal repository deletion targets for self-hosted HTTP with a stable AES-256-GCM key. Hide only delete_repository when no key is configured and expose an optional sealer interface for remote integrators. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8
Bind sealed repository deletion state to the immutable repository ID and a ten-minute expiry. Re-check identity before deletion so replay cannot affect a recreated repository. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8
…lete-repository-tool # Conflicts: # pkg/http/handler_test.go
Apply static allowlists before removing unavailable tools and fail closed on invalid configured tool names. Model independent OAuth requirements as conjunctive groups so repository deletion requires both delete_repo and repo. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8
|
Reviewed this closely because it is destructive and security-sensitive. Two blockers, both reproducible. Blocker 1: the confirmation does nothing in stdio mode
What is left is I called the handler directly with The repository is deleted in one round trip and the user is never asked anything. This is also why stdio is the primary target for this server, so the tool ships without the control it advertises. Reprofunc TestStdioModeConfirmationIsBypassable(t *testing.T) {
client := NewMockedHTTPClient(
WithRequestMatchHandler(DeleteReposByOwnerByRepo,
mockResponse(t, http.StatusNoContent, nil)),
)
deps := BaseDeps{Client: mustNewGHClient(t, client)}
require.Nil(t, deps.GetRequestStateSealer()) // stdio never configures one
tool := DeleteRepository(translations.NullTranslationHelper)
request := createMCPRequest(map[string]any{"owner": "owner", "repo": "repo"})
// Server never issued an elicitation. Client asserts the user confirmed.
request.Params.InputResponses = mcp.InputResponseMap{
deleteRepositoryConfirmationID: &mcp.ElicitResult{
Action: "accept",
Content: map[string]any{deleteRepositoryConfirmationField: "owner/repo"},
},
}
request.Params.RequestState = ""
result, err := tool.Handler(deps)(ContextWithDeps(context.Background(), deps), &request)
require.NoError(t, err)
require.False(t, result.IsError) // passes: repository deleted
}Blocker 2: scope checks changed from ANY to ALL for every tool
Three existing tools declare
Running
Consequence: the README is now wrong
// Scope filtering uses "any of" semantics (see scopes.HasRequiredScopes),
// so when multiple required scopes are listed, render them as alternatives
// rather than implying all are required.The generator was not updated, so the README documents "any of" for tools that now require all three entries:
To unblock
The availability work in |
Give stdio a process-local request-state sealer and make deletion fail closed without one. Preserve legacy any-of OAuth behavior globally while documenting and enforcing delete_repository's conjunctive delete_repo and repo requirements. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8
|
Addressed both blockers from the latest review:
Full lint and race suites pass on |
Include delete_repo in the supported OAuth scope set used by stdio login, HTTP protected-resource metadata, and tool filtering. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8
Keep delete_repo in protected-resource discovery for step-up authorization while excluding it from the default stdio OAuth grant. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8
Generate protected-resource supported scopes and the lower-risk default OAuth grant from one canonical scope definition list. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8
Move supported and default OAuth scope policy into pkg/scopes so protected-resource metadata and stdio grants derive from the scope domain package. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8
Keep workflow and codespace in protected-resource discovery while excluding both from the default OAuth grant alongside delete_repo. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b04480c-c2e9-483e-9b0f-34830b76a2f8
…lete-repository-tool
…lete-repository-tool
…lete-repository-tool
…lete-repository-tool
…lete-repository-tool
Summary
Adds a destructive
delete_repositoryMCP tool that deletes a repository only after the user enters the exactowner/reponame through elicitation. The tool is exposed only for MCP protocol2026-07-28and newer when the client supports form elicitation.Why
Repository deletion needs a stronger confirmation boundary than ordinary write operations. Self-hosted stateless HTTP deployments also need authenticated encryption for client-held MRTR request state so retries cannot alter the confirmed target.
N/A - no linked issue.
What changed
delete_repositorywith thedelete_repoOAuth scope, destructive annotations, and exact repository-name confirmation.tools/listand enforced direct calls inside registered handlers across stdio and remote HTTP transports.GITHUB_MCP_SERVER_MRTR_STATE_KEY.github.RequestStateSealerprovider contract so hosted integrations can retain their existing key format without changingToolDependencies.MCP impact
The new schema accepts
ownerandrepo; execution then requestsrepository_namethrough multi-round-trip form elicitation before calling GitHub's delete repository API. HTTP mode exposes the tool only when a valid request-state encryption key is configured.Prompts tested (tool changes only)
owner/reporepository" - automated in-memory MCP coverage verifies elicitation, exact matching, refusal paths, tamper rejection, target binding, and successful deletion.Security / limits
The tool requires the dedicated
delete_repoOAuth scope, refuses declined or mismatched confirmation, and is hidden and refused unless the request uses protocol2026-07-28or newer and advertises form elicitation support. Self-hosted HTTP uses AES-256-GCM request-state protection; missing keys hide only this tool and malformed keys fail startup.Tool renaming
deprecated_tool_aliases.goNote: if you're renaming tools, you must add the tool aliases. For more information on how to do so, please refer to the official docs.
Lint & tests
./script/lint./script/testDocs